Amplify Reactで取得したトークンを使って、Swagger UIからAPIを実行してみた
サーバーレス開発部改めCX事業本部の岩田です。 Amplify Reactを使ってCognitoユーザープールにサインインし、払い出されたトークンを使ってSwagger UIからAPIを実行できる環境を作ったので、構築手順等ご紹介します。
環境
今回利用した環境です
- Node.js v8.10
- Swagger UI 3.23.0
- React 15.6.2
- AWS Amplify 1.1.29
- AWS Amplify React 2.3.9
概要
現在開発中の案件では以下のような環境でAPIの開発を行なっています。
- API Gatewayの定義にはSwaggerを利用
- Swagger UI × S3の静的ウェブサイトホスティングで上記Swaggerの定義を公開して顧客と共有
- APIの認可にはCognitoユーザープールを利用
ちなみに、Swagger UIとはこんなツールです
見たことのある方も多いのではないでしょうか?
Swagger UIを利用することで、顧客側からも簡単にAPIをテストできるのですが、Cognitoによる認可を付けると、途端に面倒になります。APIを実行する前にサインインし、払い出されたトークンをAPIのリクエストヘッダーにセットする必要があります。
この辺りの手順をもっと楽チンにするべくCognitoとSwagger UIの連携について調べたところ、既にブログ化されていたのですが、今回は
- 顧客側にCognitoのアプリクライアントIDを入力してもらう必要がある
- Swaggerを使ってAPI GatewayにCognitoオーソライザーを定義する場合、
type: oauth2
ではなくtype: apiKey
として定義する必要があり、API Gatewayの定義をそのまま顧客と共有している今回の構成にマッチしない
という理由から採用を見送り、別のやり方を模索することにしました。 色々と考えた結果、Amplify Reactでサクっとサインイン画面を用意しつつ、サインイン後にSwagger UIの画面に遷移、以後のリクエストには払い出されたトークンを自動的に設定するという方式を選択しました。
API Gatewayの準備
では、ここから実際に環境を作っていきます。 あまり詳細まで解説しませんので、不明点があればこのあたりの記事も合わせてご参照下さい。
- AWS Amplify+Angular6+Cognitoでログインページを作ってみる ~バックエンド編~
- UnityでCognito UserPoolsから得たトークンでリクエストし、API Gatewayでsubを受け取る
サンプルをもとにPetStoreAPIを作成し、Cognitoオーソライザーの設定を行います
今回はList all pets
APIを変更し、Cognitoオーソライザーで認可後、後続のLambdaに処理を引き渡すよう設定しました。Lambdaはevent
オブジェクトをJSONに変換して返却するよう実装しています。
ひと通り準備できたら、APIをデプロイしてSwaggerの定義をJSONで出力しておきます。
出力したJSONファイルは静的ウェブサイトホスティングを有効化したS3バケットにアップしておきます。 アップできたら、バックエンド側は準備完了です。
Swagger UIの環境作成
続いてフロント側を準備します。
まずはアプリのひな形作成とライブラリの導入です。
$ create-react-app myapp $ cd myapp $ npm install aws-amplify aws-amplify-react swagger-ui
準備できたらApp.jsを編集します
import React, {Component} from 'react'; import SwaggerUi, {presets} from 'swagger-ui'; import 'swagger-ui/dist/swagger-ui.css'; import Amplify, {Auth} from 'aws-amplify'; import {withAuthenticator } from 'aws-amplify-react'; Amplify.configure({ Auth: { region: 'ap-northeast-1', userPoolId: '<CognitoのユーザープールID>', userPoolWebClientId: '<CognitoのアプリクライアントID>', } }); class App extends Component { componentDidMount() { Auth.currentSession() .then(data => { const idToken = data.getIdToken().getJwtToken(); SwaggerUi({ dom_id: '#swaggerContainer', url: 'https://<Swaggerの定義をアップしたS3バケット名>.s3-ap-northeast-1.amazonaws.com/PetStore-dev-swagger.json', presets: [presets.apis], requestInterceptor: (req) =>{ console.log(req); // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので // S3へのリクエストの時はリクエストを加工しない if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') !== -1){ return req; } req.headers.Authorization = idToken; return req; } }); }); } render() { return ( <div className="App"> <header className="App-header"> </header> <div id="swaggerContainer" /> </div> ); } } export default withAuthenticator(App, true);
ポイントとなるのはcomponentDidMount
の処理です。
Auth.currentSession() .then(data => { const idToken = data.getIdToken().getJwtToken();
の部分で、Cognitoから払い出されたIDトークンを取得します。
次に以下のコードでSwagger UIのコンポーネントを作成します。
SwaggerUi({ dom_id: '#swaggerContainer', url: 'https://<Swaggerの定義をアップしたS3バケット名>.s3-ap-northeast-1.amazonaws.com/PetStore-dev-swagger.json', presets: [presets.apis], requestInterceptor: (req) =>{ console.log(req); // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので // S3へのリクエストの時はリクエストを加工しない if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') === -1){ req.headers.Authorization = idToken; } return req; } });
requestInterceptor
を実装することで、デフォルトのリクエストオブジェクトを加工することができます。ここではリクエストヘッダーのAuthorization
に先ほど取得したIDトークンを設定しています。
また、S3の静的Webサイトホスティングに対してAuthorization
ヘッダーでIDトークンを送るとAuthorization header is invalid -- one and only one ' ' (space) required
という403エラーが返ってくるので、S3向けにはAuthorization
ヘッダーをセットしないように制御しています。
上記の実装だと、1時間後にCognitoのIDトークンの有効期限が切れるとリクエストが発行できなくなります。本来は
requestInterceptor = (req)=>{ return Auth.currentSession() .then(data => { // // S3の静的Webサイトホスティングに対してAuthorizationヘッダーでIDトークンを送るとエラーになるので // // S3へのリクエストの時はリクエストを加工しない if (req.url.indexOf('s3-ap-northeast-1.amazonaws.com') === -1){ const idToken = data.getIdToken().getJwtToken(); req.headers.Authorization = idToken } }) }
のようにAmplifyにトークンを自動リフレッシュさせたいのですが、requestInterceptorにPromiseを返す関数を設定するとSwagger UIに表示されるCURLコマンドが正しく生成されないという不具合があるため、タイムアウトについては我慢しています。
https://github.com/swagger-api/swagger-ui/issues/4778
実装できたらnpm start
して動かしてみましょう。
いつものサインイン画面が出てくるので、サインインすると、、、
Swagger UIの画面が表示されました!!
Cognitoオーソライザーを設定したList all pets
のAPIを叩いてみます
リクエストヘッダーにAuthorization
が設定できていることが分かります
開発者コンソールにもリクエストの情報が出力されています
レスポンスもちゃんと返ってきてますね!!
まとめ
Swagger UI標準の Authorize
が表示されっぱなしになっているのが気になりますが、まあ一般公開するページではないので我慢することにします。
Swaggerのドキュメントによると
OIDC is currently not supported in Swagger Editor and Swagger UI. Please follow this issue for updates.
とのことなので、Swagger UIがOIDCにも対応して、もっと簡単かつ良い感じにCognitoと一緒に使えると良いですね!!